🎖️GitЯра🎖️
Node / meshtastic / Meshtastic-Android / files / core / data / src / commonMain / kotlin / org / meshtastic / core / data / repository / DeviceLinkRepositoryImpl.kt
Displaying Raw • Download
core/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/DeviceLinkRepositoryImpl.kt 492afbde5c98f3efdcd367ab2f13ef3665fe7388 (492afbde) Text, 7.83 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.data.repository
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.NonCancellable
Tff7b72import T7ee787kotlinx.coroutines.coroutineScope
Tff7b72import T7ee787kotlinx.coroutines.flow.Flow
Tff7b72import T7ee787kotlinx.coroutines.flow.emitAll
Tff7b72import T7ee787kotlinx.coroutines.flow.flow
Tff7b72import T7ee787kotlinx.coroutines.flow.map
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787kotlinx.coroutines.withContext
Tff7b72import T7ee787kotlinx.serialization.json.Json
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.common.util.safeCatching
Tff7b72import T7ee787org.meshtastic.core.data.datasource.BundledAssetReader
Tff7b72import T7ee787org.meshtastic.core.data.datasource.DeviceLinkLocalDataSource
Tff7b72import T7ee787org.meshtastic.core.data.datasource.decode
Tff7b72import T7ee787org.meshtastic.core.data.util.SingleFlightRefresher
Tff7b72import T7ee787org.meshtastic.core.database.entity.asEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.asExternalModel
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.DeviceLink
Tff7b72import T7ee787org.meshtastic.core.model.NetworkDeviceLink
Tff7b72import T7ee787org.meshtastic.core.model.NetworkDeviceLinksResponse
Tff7b72import T7ee787org.meshtastic.core.model.toDeviceLink
Tff7b72import T7ee787org.meshtastic.core.model.util.TimeConstants
Tff7b72import T7ee787org.meshtastic.core.network.DeviceLinksRemoteDataSource
Tff7b72import T7ee787org.meshtastic.core.repository.DeviceLinkRepository
Tff7b72import T7ee787kotlin.concurrent.Volatile
T8b949e/**
* Caches the resolved device-links catalog from the Meshtastic API (`/resource/deviceLinks`). The server does all the
* classification (type/targets/regions), so the client just seeds from a bundled snapshot, refreshes from the network,
* and filters the cache. Mirrors [DeviceHardwareRepositoryImpl]'s seed → single-flight refresh pattern.
*/
Tf0883e@Single
Tff7b72class T56d364DeviceLinkRepositoryImplTb4b4b4(
Tff7b72private Tff7b72val Te6edf3remoteDataSourceTb4b4b4: Te6edf3DeviceLinksRemoteDataSourceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3assetReaderTb4b4b4: Te6edf3BundledAssetReaderTb4b4b4,
Tff7b72private Tff7b72val Te6edf3jsonTb4b4b4: Te6edf3JsonTb4b4b4,
Tff7b72private Tff7b72val Te6edf3localDataSourceTb4b4b4: Te6edf3DeviceLinkLocalDataSourceTb4b4b4,
Tff7b72private Tff7b72val Te6edf3dispatchersTb4b4b4: Te6edf3CoroutineDispatchersTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3DeviceLinkRepository Tb4b4b4{
T8b949e/** Serializes seeding and network refreshes so concurrent collectors don't duplicate writes. */
Tff7b72private Tff7b72val Te6edf3writeMutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)
Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3lastRefreshMillis Tff7b72= T79c0ff0L
T8b949e/** Single-flights stale-triggered refreshes; the TTL is re-checked inside so late joiners don't re-fetch. */
Tff7b72private Tff7b72val Te6edf3staleRefresher Tff7b72=
Te6edf3SingleFlightRefresherTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4, Ta5d6ff"Ta5d6ffDeviceLinkRepositoryTa5d6ff"Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3nowMillis Tff7b72- Te6edf3lastRefreshMillis Tff7b72> Te6edf3CACHE_EXPIRATION_TIME_MSTb4b4b4) Te6edf3reconcileTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffensureImportedTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3ensureSeededTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffreconcileTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3safeCatching Tb4b4b4{
T8b949e// The network call is bounded by the HttpClient's own timeout/retry policy — api.meshtastic.org has
T8b949e// been measured taking 20-60s, so a short local deadline would cancel every refresh. It runs outside
T8b949e// writeMutex so seeding (and therefore first emission) is never blocked behind a slow fetch. The DB
T8b949e// write is NonCancellable so a cancelled caller can't leave the cache half-pruned.
Tff7b72val Te6edf3remoteLinks Tff7b72= Te6edf3remoteDataSourceTb4b4b4.Te6edf3getDeviceLinksTb4b4b4(Tb4b4b4)
Te6edf3writeMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Te6edf3withContextTb4b4b4(Te6edf3NonCancellable Tff7b72+ Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
T8b949e// Only an applied payload counts as fresh — an ignored empty response must not suppress
T8b949e// retries for a whole expiration window while the cache stays stale.
Tff7b72if Tb4b4b4(Te6edf3storeTb4b4b4(Te6edf3remoteLinksTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3lastRefreshMillis Tff7b72= Te6edf3nowMillis
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72> Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDeviceLinkRepository: network refresh failedTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Tff7b72override Tff7b72suspend Tff7b72fun Td2a8ffgetLinksForTargetTb4b4b4(Te6edf3platformioTargetTb4b4b4: Tffa657StringTb4b4b4, Te6edf3regionCodeTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceLinkTff7b72> Tff7b72=
Te6edf3withContextTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3platformioTargetTb4b4b4.Te6edf3isBlankTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72returnTf0883e@withContext Te6edf3emptyListTb4b4b4(Tb4b4b4)
Te6edf3ensureSeededTb4b4b4(Tb4b4b4)
T8b949e// Deliberately non-blocking: the node-detail flow must stay snappy. Freshness arrives via reconcile(),
T8b949e// triggered by the device-hardware refresh that resolves this device's hardware in the first place.
Te6edf3localDataSource
Tb4b4b4.Te6edf3getAllTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3asExternalModelTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3filter Tb4b4b4{ Te6edf3link Tff7b72-Tff7b72> Te6edf3platformioTarget Tff7b72in Te6edf3linkTb4b4b4.Te6edf3targetsTb4b4b4.Te6edf3orEmptyTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3filter Tb4b4b4{ Te6edf3link Tff7b72-Tff7b72>
Tff7b72val Te6edf3regions Tff7b72= Te6edf3linkTb4b4b4.Te6edf3regions
Te6edf3regionsTb4b4b4.Te6edf3isNullOrEmptyTb4b4b4(Tb4b4b4) Tff7b72|Tff7b72| Te6edf3regionCode Tff7b72in Te6edf3regions
Tb4b4b4}
Tb4b4b4.Te6edf3sortedByDescending Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isVendor Tb4b4b4}
Tb4b4b4}
Tff7b72override Tff7b72fun Td2a8ffobserveAllLinksTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3ListTff7b72<Te6edf3DeviceLinkTff7b72>Tff7b72> Tff7b72= Te6edf3flow Tb4b4b4{
Te6edf3ensureSeededTb4b4b4(Tb4b4b4)
Te6edf3coroutineScope Tb4b4b4{
T8b949e// Refresh concurrently so a slow API response can't delay the first emission; Room re-emits when the
T8b949e// refreshed rows land.
Te6edf3launch Tb4b4b4{ Te6edf3refreshIfStaleTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3emitAllTb4b4b4(Te6edf3localDataSourceTb4b4b4.Te6edf3observeAllTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3entities Tff7b72-Tff7b72> Te6edf3entitiesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3asExternalModelTb4b4b4(Tb4b4b4) Tb4b4b4} Tb4b4b4}Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** Seeds the table from the bundled snapshot if empty (fresh install, data clear, radio switch). */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffensureSeededTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3localDataSourceTb4b4b4.Te6edf3countTb4b4b4(Tb4b4b4) Tff7b72> T79c0ff0Tb4b4b4) Tff7b72return
Te6edf3writeMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3localDataSourceTb4b4b4.Te6edf3countTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Tb4b4b4{
Te6edf3safeCatching Tb4b4b4{
Te6edf3storeTb4b4b4(
Te6edf3assetReaderTb4b4b4.Te6edf3decodeTff7b72<Te6edf3NetworkDeviceLinksResponseTff7b72>Tb4b4b4(Ta5d6ff"Ta5d6ffdevice_links.jsonTa5d6ff"Tb4b4b4, Te6edf3jsonTb4b4b4)Tff7b72?.Te6edf3linksTb4b4b4.Te6edf3orEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3onFailure Tb4b4b4{ Te6edf3e Tff7b72-Tff7b72> Te6edf3LoggerTb4b4b4.Te6edf3wTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffDeviceLinkRepository: failed to seed from bundled JSONTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/** Best-effort network refresh, gated by [CACHE_EXPIRATION_TIME_MS] and single-flighted via [staleRefresher]. */
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffrefreshIfStaleTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3nowMillis Tff7b72- Te6edf3lastRefreshMillis Tff7b72<Tff7b72= Te6edf3CACHE_EXPIRATION_TIME_MSTb4b4b4) Tff7b72return
Te6edf3staleRefresherTb4b4b4.Te6edf3refreshTb4b4b4(Tb4b4b4)
Tb4b4b4}
T8b949e/**
* Maps resolved API links to the cached domain model, upserts them, and prunes short codes that no longer exist.
* Internal links (GitHub, YouTube, …) are dropped — they never belong to a device's purchase section. An empty list
* is ignored rather than wiping the cache on a bad response. Returns whether anything was stored.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffstoreTb4b4b4(Te6edf3networkLinksTb4b4b4: Te6edf3ListTff7b72<Te6edf3NetworkDeviceLinkTff7b72>Tb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72val Te6edf3links Tff7b72= Te6edf3networkLinksTb4b4b4.Te6edf3filter Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3type Tff7b72!Tff7b72= Te6edf3NetworkDeviceLinkTb4b4b4.Te6edf3TYPE_INTERNAL Tb4b4b4}Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3toDeviceLinkTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3linksTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffDeviceLinkRepository: no device links to store; leaving cache untouchedTa5d6ff" Tb4b4b4}
Tff7b72return Tff7b72false
Tb4b4b4}
Te6edf3localDataSourceTb4b4b4.Te6edf3upsertAllTb4b4b4(Te6edf3linksTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3asEntityTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4)
Te6edf3localDataSourceTb4b4b4.Te6edf3deleteNotInTb4b4b4(Te6edf3linksTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3shortCode Tb4b4b4}Tb4b4b4)
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffDeviceLinkRepository: stored Tffd700${Te6edf3linksTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff device linksTa5d6ff" Tb4b4b4}
Tff7b72return Tff7b72true
Tb4b4b4}
Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72private Tff7b72val Te6edf3CACHE_EXPIRATION_TIME_MS Tff7b72= Te6edf3TimeConstantsTb4b4b4.Te6edf3ONE_DAYTb4b4b4.Te6edf3inWholeMilliseconds
Tb4b4b4}
Tb4b4b4}
Served by rngit 1.4.2 - Generated in 0.32s